
/* GCSx
** FILEDIALOG.CPP
**
** Standard file open/save/save as dialogs
*/

/*****************************************************************************
** Copyright (C) 2003-2006 Janson
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
** 
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
*****************************************************************************/

#include "all.h"

// Last-used directories
string* previousDirectories = NULL;

// File type associated extensions for saving, and some for loading
vector<string>* fileExtensions = NULL;

// Standard file dialog
class FileDirDialog : public Dialog {
private:
    int initialized;
    int initializedH;
    int initializedW;
    int selection;
    int saveAs;
    int openForWrite;
    string file;
    DirectoryView* tree;
    WListBox* list;
    WTextBox* filename;
    const vector<string>* currentExtensions;

    int treeviewEvent(int code, int command, int check);
    static int treeviewEventWrap(void* ptr, int code, int command, int check);
    void refillList(const string* itemToSelect = NULL);

#if !FILESYSTEM_CASE_SENSITIVE
    struct stringCmpI : public binary_function<string, string, bool> {
    	bool operator()(const string& x, const string& y) { return myStricmp(x.c_str(), y.c_str()) < 0; }
    };
#endif

public:
    enum {
        ID_TREE = 1,
        ID_LIST,
        ID_LABEL,
        ID_ENTRY,
        ID_OK,
        ID_CANCEL,
    };

    FileDirDialog();
    virtual ~FileDirDialog();
    void childModified(Window* modified);
    virtual ButtonAction verifyEntry(int buttonId, ButtonAction buttonType);
    void firstControl();

    int run(int isSaveAs, int isOpenForWrite, const vector<string>& extensions, string& storeFilename);
} *fileDialog = NULL;

// Initializes extensions, last-used directories and dialog
void initFileSystems() { start_func
    if (!fileExtensions) {
        previousDirectories = new string[FILETYPE_COUNT];
        fileExtensions = new vector<string>[FILETYPE_COUNT];
        fileDialog = new FileDirDialog;
        
        char cwd[PATH_MAX];
        getcwd(cwd, PATH_MAX);
        
        for (int pos = 0; pos < FILETYPE_COUNT; ++pos) {
            previousDirectories[pos] = cwd;
        }

        initFileExtensions(fileExtensions);
    }
}

// Cleanup

void destroyFileDialogGlobals() { start_func
    if (previousDirectories) delete[] previousDirectories;
    if (fileExtensions) delete[] fileExtensions;
    if (fileDialog) delete fileDialog;

    previousDirectories = NULL;
    fileExtensions = NULL;
    fileDialog = NULL;
}

// Directory treeview

DirectoryView::DirectoryView(const string& name, const char* fullpath, void* tPtr, int (*tAction)(void* ptr, int code, int command, int check), int topLevel) : TreeView(name, tPtr, 0, tAction, topLevel, FILESYSTEM_CASE_SENSITIVE), path(blankString) { start_func
    path = fullpath;
    hasSubDirs = existFiles(fullpath, NULL);

    if ((FILESYSTEM_DRIVE_SEP) && (name[1] == FILESYSTEM_DRIVE_SEP) && (name.length() == 2)) {
        iconOpen = 2;
        iconClosed = 2;
    }
    else {
        iconOpen = 0;
        iconClosed = 1;
    }
}

DirectoryView::~DirectoryView() { start_func
}

void DirectoryView::redo() { start_func
    removeAll();
    hasSubDirs = existFiles(path.c_str(), NULL);
    expand(1);
    if (hasSubItems()) {
        subtree[0]->selectMe(); // (start with top item selected)
    }
    else {
        selectMe(); // (start with self selected)
    }
}

int DirectoryView::hasSubItems() const { start_func
    return (subitems > 0) || (hasSubDirs);
}

void DirectoryView::expand(int state) { start_func
    if ((hasSubDirs) && (state)) {
        vector<string> subdirs;
        
        if (findFiles(path.c_str(), NULL, subdirs)) {
            vector<string>::iterator end = subdirs.end();
            DirectoryView* subdir = NULL;
            
            for (vector<string>::iterator pos = subdirs.begin(); pos != end; ++pos) {
                string newpath;
                createDirname(path.c_str(), (*pos).c_str(), newpath);

                subdir = new DirectoryView(*pos, newpath.c_str(), ptr, action);
                insert(subdir, 0);
                subdir = NULL;
            }

            hasSubDirs = 0;
        }
        else {
            hasSubDirs = 0;
            // We thought we had subdirs, but we didn't; however
            // we're dirty as our icon needs to vanish
            setDirty();
            return;
        }
    }

    TreeView::expand(state);
}

// Standard file dialog
FileDirDialog::FileDirDialog() : Dialog(blankString), file(blankString) { start_func
    initialized = 0;
    tree = NULL;
    list = NULL;
    filename = NULL;
    currentExtensions = NULL;
}

FileDirDialog::~FileDirDialog() { start_func
    // (dialog/wscroll will not delete tree for us)
    // (we must clear widgets first, so our tree doesn't get referenced in ~Dialog())
    filename = NULL;
    list = NULL;
    clearWidgets();
    delete tree;
}

void FileDirDialog::firstControl() { start_func
    changeInputFocus(NULL);
    nextControl();
    if (saveAs) {
        nextControl();
        nextControl();
    }
}

void FileDirDialog::childModified(Window* modified) { start_func
    Dialog::childModified(modified);

    if ((modified == list) && (filename)) {
        // Refill textbox
        list->apply();
        const ListEntry* entry = list->findEntry(selection);
        
        if (entry) filename->state(entry->label);
    }
}

void FileDirDialog::refillList(const string* itemToSelect) { start_func
    DirectoryView* sel = dynamic_cast<DirectoryView*>(tree->findSelected());
    list->clear();
    
    if (sel) {
        // Fill listbox
        vector<string> subfiles;
        const char* path = sel->getFullpath();
        if (currentExtensions) {
            vector<string>::const_iterator extEnd = currentExtensions->end();
            for (vector<string>::const_iterator extPos = currentExtensions->begin(); extPos != extEnd; ++extPos) {
                string extMask(FILESYSTEM_EXTENSIONSEP);
                extMask += *extPos;
                findFiles(path, extMask.c_str(), subfiles);
            }
        }
            
        if (!subfiles.empty()) {
#if FILESYSTEM_CASE_SENSITIVE
            sort(subfiles.begin(), subfiles.end());
#else
            sort(subfiles.begin(), subfiles.end(), stringCmpI());
#endif

            vector<string>::iterator end = subfiles.end();
            
            int id = 0;
            for (vector<string>::iterator pos = subfiles.begin(); pos != end; ++pos) {
                list->addItem(ListEntry(*pos, ++id));
#if FILESYSTEM_CASE_SENSITIVE
                if ((itemToSelect) && (!strcmp(itemToSelect->c_str(), (*pos).c_str()))) selection = id;
#else
                if ((itemToSelect) && (!myStricmp(itemToSelect->c_str(), (*pos).c_str()))) selection = id;
#endif
            }
        }
        
        // Empty textbox
        if (filename) filename->state(blankString);
    }
}

int FileDirDialog::treeviewEvent(int code, int command, int check) { start_func
    if (check) return Window::COMMAND_HIDE;

    if (command == LV_MOVE) {
        refillList();
        return 1;
    }
    
    return 0;
}

int FileDirDialog::treeviewEventWrap(void* ptr, int code, int command, int check) { start_func
    return ((FileDirDialog*)ptr)->treeviewEvent(code, command, check);
}

Dialog::ButtonAction FileDirDialog::verifyEntry(int buttonId, ButtonAction buttonType) { start_func
    if ((buttonType != BUTTON_APPLY) && (buttonType != BUTTON_OK)) return BUTTON_DEFAULT;

    DirectoryView* sel = dynamic_cast<DirectoryView*>(tree->findSelected());
    const char* path = sel->getFullpath();

    // Allows entry of name by itself or a relative pathname or an absolute pathname
    string fullname;
    createFilename(path, filename->state().c_str(), fullname);
    
    // Add extension if none given and saveas
    if ((currentExtensions) && (saveAs)) {
        string::size_type found = filename->state().find_last_of(FILESYSTEM_SEPARATORS);
        if (found >= string::npos) found = 0;
        if (filename->state().find_first_of(FILESYSTEM_EXTENSIONSEP) >= string::npos) {
            fullname += FILESYSTEM_EXTENSIONSEP;
            fullname += (*currentExtensions)[0];
        }
    }
    
    // Store entry back in textbox
    filename->state(fullname);

    if (saveAs) {
        // Does file exist?
        FILE* exist = fopen(fullname.c_str(), "rb");
        if (exist) {
            int result = guiConfirmBox("File already exists- Overwrite?", "Confirm Overwrite");
            fclose(exist);
            
            if (!result) return BUTTON_NOTHING;
    
            // Attempt to write to file
            FILE* attempt = fopen(fullname.c_str(), "rb+");
            if (!attempt) {
                guiErrorBox(formatString("Error writing to file- %s", strerror(errno)), errorTitleException);
                return BUTTON_NOTHING;
            }
            fclose(attempt);
        }
        else {
            // Attempt to create file
            FILE* attempt = fopen(fullname.c_str(), "wb+");
            if (!attempt) {
                guiErrorBox(formatString("Error creating file- %s", strerror(errno)), errorTitleException);
                return BUTTON_NOTHING;
            }
            fclose(attempt);
        }
    }
    else {
        // Ensure file exists, and if applicable, can be written to
        FILE* exist = fopen(fullname.c_str(), openForWrite ? "rb+" : "rb");
        if (!exist) {
            guiErrorBox(formatString("Error opening file- %s", strerror(errno)), errorTitleException);
            return BUTTON_NOTHING;
        }
        fclose(exist);
    }

    return Dialog::verifyEntry(buttonId, buttonType);
}

int FileDirDialog::run(int isSaveAs, int isOpenForWrite, const vector<string>& extensions, string& storeFilename) { start_func
    // Different screen height?
    if ((initialized) && ((initializedH != screenHeight) || (initializedW != screenWidth))) {
        filename = NULL;
        list = NULL;
        clearWidgets();
        delete tree;
        tree = NULL;
        initialized = 0;
    }

    if (!initialized) {
        initializedH = screenHeight;
        initializedW = screenWidth;
        Widget* w = NULL;
        
        int innerHeight = initializedH - (fontHeight() * 10);
        if (innerHeight > 400) innerHeight = 400;
        
        int innerWidth = initializedW - 40;
        if (innerWidth > 400) innerWidth = 400;

        w = new WStatic(ID_LABEL, "Directory:");
        w->addTo(this);

        w = new WStatic(ID_LABEL, "Files:");
        w->addTo(this);

        // w contains pointer so it gets deleted if it doesn't get added
        // Must create list here because creating directoryview accesses it
        // (in resulting treeview events)
        w = list = new WListBox(ID_LIST, &selection, 1, innerWidth / 2, innerHeight / fontHeight());

        tree = new DirectoryView(blankString, topDir(), this, treeviewEventWrap, 1);
        tree->addTo(this, innerWidth / 2, innerHeight / tree->getItemHeight());
        // (wscroll will not delete tree for us)
        
        w->addTo(this);
        
        makePretty(2, 1, 1, 0);

        w = new WStatic(ID_LABEL, "Filename:");
        innerWidth -= w->getWidth();
        w->addTo(this);

        w = filename = new WTextBox(ID_ENTRY, &file, 0, innerWidth);
        w->addTo(this);

        w = new WButton(ID_OK, isSaveAs ? "Save" : "Open", BUTTON_OK);
        w->addTo(this);
        
        w = new WButton(ID_CANCEL, messageBoxCancel, BUTTON_CANCEL);
        w->addTo(this);
        
        makePretty();
        
        initialized = 1;
    }
    else {
        // Redo treeview
        assert(tree);
        tree->redo();
        
        // Redo buttons
        dynamic_cast<WButton*>(findWidget(ID_OK))->changeText(isSaveAs ? "Save" : "Open");
    }
    
    setTitle(isSaveAs ? "Save As" : "Open File");    
    selection = 0;
    file = blankString;
    saveAs = isSaveAs;
    openForWrite = isOpenForWrite;
    currentExtensions = &extensions;

    // Split filename and select within tree
    vector<string> split;
    splitFilename(storeFilename.c_str(), split);
    
    vector<string>::iterator end = split.end();
    DirectoryView* currentTree = tree;
    int refilledList = 0;

    for (vector<string>::iterator pos = split.begin(); pos != end; ++pos) {
        currentTree->expand(1);
        currentTree = dynamic_cast<DirectoryView*>(currentTree->find(*pos));
        if (currentTree) {
            currentTree->selectMe();
        }
        else {
            // This will be a final item, check as a filename
            refillList(&(*pos));
            refilledList = 1;
            break;
        }
    }
    
    // List should be filled if it wasn't during filename scan
    if (!refilledList) refillList();
    if (runModal() == ID_OK) {
        storeFilename = file;
        currentExtensions = NULL;
        return 1;
    }
    currentExtensions = NULL;
    return 0;
}

int fileOpen(int type, int requireWriteAccess, string& filename) { start_func
    assert(type >= 0);
    assert(type < FILETYPE_COUNT);

    initFileSystems();
    
    if (filename.empty()) filename = previousDirectories[type];    
    if (fileDialog->run(0, requireWriteAccess, fileExtensions[type], filename)) {
        getPathname(filename.c_str(), previousDirectories[type]);
        return 1;
    }
    return 0;
}

int fileSaveAs(int type, string& filename) { start_func
    assert(type >= 0);
    assert(type < FILETYPE_COUNT);

    initFileSystems();
    
    if (filename.empty()) filename = previousDirectories[type];
    if (fileDialog->run(1, 1, fileExtensions[type], filename)) {
        getPathname(filename.c_str(), previousDirectories[type]);
        return 1;
    }
    return 0;
}
